// Copyright (c) 2021 Terminus, Inc.
//
// This program is free software: you can use, redistribute, and/or modify
// it under the terms of the GNU Affero General Public License, version 3
// or later ("AGPL"), as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package main

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"unicode"

	"google.golang.org/genproto/googleapis/api/annotations"
	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/proto"
)

const (
	contextPackage   = protogen.GoImportPath("context")
	grpcPackage      = protogen.GoImportPath("google.golang.org/grpc")
	transgrpcPackage = protogen.GoImportPath("github.com/erda-project/erda-infra/pkg/transport/grpc")
)

func generateFiles(gen *protogen.Plugin, files []*protogen.File) error {
	sort.Slice(files, func(i, j int) bool {
		return files[i].Desc.Name() < files[j].Desc.Name()
	})
	var paths []string
	for _, f := range files {
		if len(f.Services) <= 0 {
			continue
		}
		paths = append(paths, f.Desc.Path())
	}
	sources := strings.Join(paths, ", ")

	const filename = "apis.go"
	g := gen.NewGeneratedFile(filename, protogen.GoImportPath(*pkgName))
	g.P("// Code generated by ", genName, ". DO NOT EDIT.")
	g.P("// Sources: ", sources)
	g.P()
	g.P("package ", *pkgName)
	g.P()
	g.P("// RegisterAPIs register all apis")
	g.P("func RegisterAPIs(add func(method, path, backendPath, service string)) {")
	for i, file := range files {
		if len(file.Services) <= 0 {
			continue
		}
		for _, ser := range file.Services {
			_, ok := findTag(privateTagKey, ser.Comments.Leading)
			if ok {
				continue
			}
			svrTags, publishSvr := findTag(publishTagKey, ser.Comments.Leading)
			var methods []*publishMethod
			for _, method := range ser.Methods {
				if method.Desc.IsStreamingClient() || method.Desc.IsStreamingServer() {
					continue
				}
				_, ok := findTag(privateTagKey, method.Comments.Leading)
				if ok {
					continue
				}
				tags, ok := findTag(publishTagKey, method.Comments.Leading)
				if ok || publishSvr {
					methods = append(methods, &publishMethod{
						Method: method,
						tag:    Tags{tags, svrTags},
					})
				}
			}
			if len(methods) > 0 {
				g.P("// source: ", file.Desc.Path(), " service: ", ser.Desc.Name())
				for _, method := range methods {
					rule, ok := proto.GetExtension(method.Desc.Options(), annotations.E_Http).(*annotations.HttpRule)
					if rule != nil && ok {
						var pubAdd bool
						additional := method.tag.Get("additional")
						if len(additional) > 0 {
							b, err := strconv.ParseBool(additional)
							if err != nil {
								return fmt.Errorf("invalid additional value %q: %s", additional, err)
							}
							pubAdd = b
						}
						if pubAdd {
							for _, bind := range rule.AdditionalBindings {
								err := genMethodAPI(g, file, ser, method, bind)
								if err != nil {
									return err
								}
							}
						}
						err := genMethodAPI(g, file, ser, method, rule)
						if err != nil {
							return err
						}
					} else {
						err := genMethodAPI(g, file, ser, method, nil)
						if err != nil {
							return err
						}
					}
				}
			}
		}
		if i < len(files)-1 {
			g.P()
		}
	}
	g.P("}")
	return nil
}

func findTag(key string, comments protogen.Comments) (Tag, bool) {
	list := strings.Split(strings.TrimSuffix(string(comments), "\n"), "\n")
	for i, n := 0, len(list); i < n; i++ {
		line := strings.TrimSpace(list[i])
		if strings.HasPrefix(line, "+") {
			if strings.HasPrefix(line[1:], key) {
				line = line[1+len(key):]
				tag := strings.TrimSpace(line)
				if len(tag) <= 0 {
					return reflect.StructTag(""), true
				}
				if unicode.IsSpace([]rune(line)[0]) {
					return reflect.StructTag(tag), true
				}
			}
		}
	}
	return reflect.StructTag(""), false
}

type Tag interface {
	Get(key string) string
	Lookup(key string) (value string, ok bool)
}

type Tags []Tag

func (tags Tags) Get(key string) string {
	for _, t := range tags {
		val, ok := t.Lookup(key)
		if ok {
			return val
		}
	}
	return ""
}

func (tags Tags) Lookup(key string) (value string, ok bool) {
	for _, t := range tags {
		val, ok := t.Lookup(key)
		if ok {
			return val, true
		}
	}
	return "", false
}

type publishMethod struct {
	*protogen.Method
	tag Tag
}

func genMethodAPI(g *protogen.GeneratedFile, file *protogen.File, service *protogen.Service, m *publishMethod, rule *annotations.HttpRule) error {
	var path, method string
	if rule != nil {
		switch pattern := rule.Pattern.(type) {
		case *annotations.HttpRule_Get:
			path = pattern.Get
			method = "GET"
		case *annotations.HttpRule_Put:
			path = pattern.Put
			method = "PUT"
		case *annotations.HttpRule_Post:
			path = pattern.Post
			method = "POST"
		case *annotations.HttpRule_Delete:
			path = pattern.Delete
			method = "DELETE"
		case *annotations.HttpRule_Patch:
			path = pattern.Patch
			method = "PATCH"
		case *annotations.HttpRule_Custom:
			path = pattern.Custom.Path
			method = pattern.Custom.Kind
		}
	}
	if len(path) <= 0 {
		path = fmt.Sprintf("/%s/%s", service.Desc.FullName(), m.Desc.Name())
	} else {
		path = formatPath(path)
	}
	if len(method) <= 0 {
		method = "POST"
	}
	serviceName := m.tag.Get("service")
	if len(serviceName) <= 0 {
		serviceName = strings.TrimRight(string(file.Desc.Package()), ".") + "." + service.GoName
	}
	backPrefix := m.tag.Get("backend-prefix")
	if len(backPrefix) > 0 {
		backPrefix = formatPath(backPrefix)
		if !strings.HasPrefix(path, backPrefix) {
			return fmt.Errorf("backend path %q must has prefix %q", path, backPrefix)
		}
	}

	pubPath := m.tag.Get("path")
	if len(pubPath) > 0 {
		pubPath = formatPath(pubPath)
	} else if len(backPrefix) > 0 {
		pubPath = formatPath(path[len(backPrefix):])
	} else {
		pubPath = path
	}
	prefix := m.tag.Get("prefix")
	if len(prefix) > 0 {
		pubPath = strings.TrimRight(prefix, "/") + "/" + strings.TrimLeft(pubPath, "/")
	}
	g.P("add(", strconv.Quote(method), ", ", strconv.Quote(pubPath), ", ", strconv.Quote(path), ", ", strconv.Quote(serviceName), ")")
	return nil
}

func formatPath(path string) string {
	return "/" + strings.TrimLeft(path, "/")
}
